package org.ricks.log;

import org.ricks.utils.StrUtil;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.*;
import java.util.regex.Pattern;
import static org.ricks.log.LogPolicy.DAILY;
import static org.ricks.log.LogPolicy.STARTUP;


public final class ServiceLoader<T> {

//    private static final String SERVICE_PREFIX = "META-INF/services/";
    private static final Pattern SPLIT_PATTERN = Pattern.compile(" ");

    private final Class<? extends T> service;


    private final Class<?>[] argumentTypes;

    private final ClassLoader classLoader;
    private final Collection<String> classes;

    
    public ServiceLoader(final Class<? extends T> service, final Class<?>... argumentTypes) {
        this.service = service;
        this.argumentTypes = argumentTypes;
        this.classLoader = RuntimeProvider.getClassLoader();
        this.classes = loadClasses(classLoader, service);
    }

    
    public T create(final String name, final Object... arguments) {
        if (StrUtil.equals(name,STARTUP.name(),true)) {
            return (T) new StartupPolicy();
        } else if(StrUtil.equals(name,DAILY.name(),true)) {
            return (T) new DailyPolicy();
        } else {
            InternalLogger.log(Level.ERROR, "Service implementation '" + name + "' not found");
            return null;
        }
    }

    
    public List<T> createList(final String list) {
        List<T> instances = new ArrayList<T>();
        for (String entry : list.split(",")) {
            entry = entry.trim();
            if (!entry.isEmpty()) {
                T instance;

                int separator = entry.indexOf(':');
                if (separator == -1) {
                    instance = create(entry, (Object) null);
                } else {
                    String name = entry.substring(0, separator).trim();
                    String argument = entry.substring(separator + 1).trim();
                    instance = create(name, argument);
                }

                if (instance != null) {
                    instances.add(instance);
                }
            }
        }
        return instances;
    }

    
    public Collection<T> createAll(final Object... arguments) {
        Collection<T> instances = new ArrayList<T>(classes.size());

        for (String className : classes) {
            T instance = createInstance(className, arguments);
            if (instance != null) {
                instances.add(instance);
            }
        }

        return instances;
    }

    
    private static <T> Collection<String> loadClasses(final ClassLoader classLoader, final Class<? extends T> service) {
        String name = service.getName();
        Enumeration<URL> urls;
        try {
            urls = classLoader.getResources(name);
        } catch (IOException ex) {
            InternalLogger.log(Level.ERROR, "Failed loading services from '" + name + "'");
            return Collections.emptyList();
        }

        Collection<String> classes = new ArrayList<String>();

        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            BufferedReader reader = null;

            try {
                reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));
                for (String line = reader.readLine(); line != null; line = reader.readLine()) {
                    line = line.trim();
                    if (line.length() > 0 && line.charAt(0) != '#' && !classes.contains(line)) {
                        classes.add(line);
                    }
                }
            } catch (IOException ex) {
                InternalLogger.log(Level.ERROR, "Failed reading service resource '" + url + "'");
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException ex) {

                    }
                }
            }
        }

        return classes;
    }

    
    public String toSimpleClassName(final String name) {
        StringBuilder builder = new StringBuilder(name.length());
        for (String token : SPLIT_PATTERN.split(name)) {
            if (!token.isEmpty()) {
                builder.append(Character.toUpperCase(token.charAt(0)));
                builder.append(token.substring(1).toLowerCase(Locale.ROOT));
            }
        }
        builder.append(service.getSimpleName());
        return builder.toString();
    }

    
    @SuppressWarnings("unchecked")
    private T createInstance(final String className, final Object... arguments) {
        try {
            Class<?> implementation = Class.forName(className, false, classLoader);
            if (service.isAssignableFrom(implementation)) {
                return (T) implementation.getDeclaredConstructor(argumentTypes).newInstance(arguments);
            } else {
                InternalLogger.log(Level.ERROR, "Class '" + className + "' does not implement service interface '" + service + "'");
            }
        } catch (ClassNotFoundException ex) {
            InternalLogger.log(Level.ERROR, "Service implementation '" + className + "' not found");
        } catch (NoSuchMethodException ex) {
            InternalLogger.log(Level.ERROR, "Service implementation '" + className + "' has no matching constructor");
        } catch (InstantiationException ex) {
            InternalLogger.log(Level.ERROR, "Service implementation '" + className + "' is not instantiable");
        } catch (IllegalAccessException ex) {
            InternalLogger.log(Level.ERROR, "Constructor of service implementation '" + className + "' is not accessible");
        } catch (IllegalArgumentException ex) {
            InternalLogger.log(Level.ERROR, "Illegal arguments for constructor of service implementation '" + className + "'");
        } catch (InvocationTargetException ex) {
            InternalLogger.log(Level.ERROR, ex.getTargetException(), "Failed creating service implementation '" + className + "'");
        }

        return null;
    }

}
